Skip to content

feat(tabs): per-tab lockedRepos with independent lock lists#78

Merged
wgordon17 merged 28 commits into
gordon-code:mainfrom
wgordon17:feat/per-tab-locks
Apr 23, 2026
Merged

feat(tabs): per-tab lockedRepos with independent lock lists#78
wgordon17 merged 28 commits into
gordon-code:mainfrom
wgordon17:feat/per-tab-locks

Conversation

@wgordon17
Copy link
Copy Markdown
Member

Summary

  • Changes lockedRepos from flat global array to per-tab z.record for independent lock lists per tab
  • 3-shape migration handles pre-unified, unified, and new per-tab record formats
  • Lock functions accept tabKey; EmptyLockedRepoRow also updated (plan gap caught via typecheck)

Closes #76

Components 1-6 of custom tabs implementation:
- CustomTabSchema + BUILTIN_TAB_IDS + isBuiltinTab in schemas.ts
- Config CRUD: addCustomTab, updateCustomTab, removeCustomTab, reorderCustomTab
- View state: customTabFilters, expandedRepos widened to z.record, dynamic helpers
- TabBar: TabId=string, custom tab rendering, + button, edit pencil
- DashboardPage: custom tab routing, exclusiveOwnership, visibleIssues/PRs/Runs
- Shared filter predicates: isIssueVisible, isPrVisible, isRunVisible
- FilterBar: hideOrgRepo prop for custom tab context
- 1863 tests (88 new), typecheck clean
- Adds customTabId and filterPreset props to IssuesTab, PullRequestsTab, ActionsTab
- activeFilters memo with merge chain: schema defaults → preset → stored
- Dispatches filter read/write/reset to customTabFilters or tabFilters
- Dynamic tabKey() replaces all expandedRepos dot-notation accesses
- Updates createReorderHighlight and createFlashDetection signals
- globalFilter bypassed (null) when custom tab active
- showScopeFilter returns true for custom tabs
- Resolves _self sentinel in user filter preset
- DashboardPage passes customTabId and filterPreset to custom tab renders
- CustomTabModal: Kobalte Dialog for create/edit custom tabs
- Name, baseType, org/repo scope, filter presets, exclusive toggle
- Filter preset fields derived from shared FilterChipGroupDef arrays
- Extracts filter group constants to filterTypes.ts (single source)
- Wires modal into DashboardPage replacing placeholder
- _self sentinel in user filter preset for authenticated user
- CustomTabsSection: table with name, type badge, scope, exclusive,
  edit/delete/reorder actions
- Integrates into SettingsPage after Tabs section
- Updates handleExportSettings to include customTabs
- Updates defaultTab dropdown to include custom tab options
- Delete with window.confirm, disabled add at 10-tab cap
- Adds createEffect to reset all form signals when modal opens
  with a different editingTab (prevents stale state between edits)
- Removes unnecessary orgList memo
- Fixes resetCustomTabFilters import source
Security (SEC-001, SEC-002, SEC-006, SEC-008):
- Constrains tab ID to alphanumeric/dash/underscore regex
- Constrains orgScope entries to REPO_SEGMENT regex
- Caps orgScope and repoScope at 100 entries
- Prevents ID mutation via updateCustomTab Object.assign

Accessibility (UI-002, UI-003, UI-005):
- Associates name input and type select with labels (for/id)
- Adds aria-expanded and aria-controls to scope collapsible
- Adds aria-label to filter preset select dropdowns
- README: adds Custom Tabs section to feature list
- USER_GUIDE: adds Custom Tabs subsection with creation,
  scope, filter presets, exclusivity, and management details
- CustomTabModal: 31 tests covering create/edit mode, validation,
  base type switching, scope selection, filter presets, exclusive toggle
- CustomTabsSection: 27 tests covering table rendering, delete with
  confirm, reorder, empty state, cap enforcement, edit/add buttons
- Total: 1927 tests (152 new from baseline)
Investigated Kobalte Tabs source: keyboard navigation uses
Collection-based delegates and querySelector('[data-key=...]')
for focus management — neither depend on direct children of
Tabs.List. Wrapper divs around custom tab triggers are safe.
Updated comment to reflect verified finding.
- Exclusive issues tab removes claimed items from built-in badge
- Exclusive tab shows claimed items in its own badge count
- Non-exclusive tabs do not affect built-in tab counts
- First exclusive tab wins on overlapping scope
- Type isolation: exclusive issues tab does not affect PRs/Actions
- 1933 tests total (158 new from baseline)
- PERF-003: hoists Zod filter defaults to module-level constants
  (avoids IssueFiltersSchema.parse({}) on every memo invocation)
- Adversarial-3: customTabData only filters the relevant baseType
  per tab (was filtering all 3 types unconditionally)
- UI-007: empty state text no longer references hidden + button
- CR-014: createEffect closes modal when editingTabId references
  a deleted tab (prevents stale edit form)
- UI-004: removes role='switch' from exclusive toggle (redundant
  on native checkbox), adds aria-describedby for description text
- Updates test to query by id instead of removed role
- Delete button now shows inline 'Delete? [Yes] [No]' confirmation
  replacing the action buttons in the table row
- Removes window.confirm dependency (blocks main thread, poor a11y,
  inconsistent with Kobalte Dialog pattern used elsewhere)
- Updates tests to verify inline confirmation flow
- Correctness: case-insensitive repoScope matching, stale defaultTab
  cleanup in loadConfig, tabCounts badge applies filterPreset
- Security: updateCustomTab validates via CustomTabSchema.safeParse
- Performance: single-pass ignoredItems, visible* short-circuit on
  empty ownership, customTabData skips non-exclusive inactive tabs,
  hoisted Zod defaults outside loop
- Quality: extract createTabFilterHandlers, formatScopeSummary,
  module-level baseTypeLabel/baseTypeBadgeClass, typed filterGroupsByType,
  editingTab createMemo, removed triple blank line
- Tests: 37 new tests covering scoping, stale tab fallback, runtime
  redirect, orphan cleanup, schema validation, customTabId filter
  presets, scope cascade, hideOrgRepo, badge count integration
- Add scope (involves_me), user (surfacedBy), and sizeCategory filters
  to tabCounts badge computation for full parity with tab components
- Gate checkStatus filter on pr.enriched (matches PullRequestsTab)
- Export KNOWN_CONCLUSIONS and KNOWN_EVENTS from filterTypes.ts to
  eliminate magic array duplication between ActionsTab and DashboardPage
- Narrow baseTypeLabel/baseTypeBadgeClass parameter to union type
- Add loadConfig stale-defaultTab cleanup tests
- fix PersonalSummaryStrip receiving unfiltered data instead of visible* memos
- fix duplicate scope option in CustomTabModal filter preset dropdown
- fix removeCustomTab not resetting viewState.lastActiveTab
- extract buildTabScopeMatcher to deduplicate scope logic in DashboardPage
- extract mergeActiveFilters to deduplicate filter merge in 3 tab components
- hoist Zod filter schema defaults to module scope in DashboardPage
- simplify CustomTabModal handleSave guard and getPresetValue
- fix TabBar bracket notation, section numbering, local type alias
- add tests for formatScopeSummary, createTabFilterHandlers, mergeActiveFilters,
  matchesScope OR semantics, _self sentinel in tabCounts, SettingsPage dropdown
- tabCounts now uses mergeActiveFilters() instead of inline merge (dedup)
- add enriched guard to PR role filter in tabCounts (prevents count
  mismatch during light-phase when reviewerLogins/assigneeLogins are empty)
- restore 'All (default)' option for non-scope filter groups in
  CustomTabModal (fix regression from duplicate scope option removal)
- add removeCustomTab lastActiveTab reset tests (matching + non-matching)
- wraps TabBar +/edit and CustomTabsSection action buttons
  in Tooltip (matches RepoLockControls pattern)
- replaces distracting pencil character with subtle cog SVG
- changes placeholder from 'My OSAC PRs' to 'Needs Review'
- up/down tooltips show 'Already at top/bottom' when disabled
  (matches RepoLockControls pattern)
- TabBar edit tooltip simplified to 'Edit tab'
- TabBar edit icon uses same pencil SVG as CustomTabsSection
- 'Edit' → 'Edit "Tab Name"', 'Delete' → 'Delete "Tab Name"'
  (matches TrackedUsersSection pattern)
Resolves conflicts in ActionsTab, IssuesTab, and PullRequestsTab
by combining upstream empty-locked-repo-row feature (isEmpty guard,
EmptyLockedRepoRow fallback, flattened Show structure) with
custom-tabs dynamic tabKey() and activeFilters() accessors.
Change lockedRepos from z.array(z.string()) to z.record(z.string(),
z.array(z.string())) so each tab (built-in and custom) has independent
lock lists and ordering.

Schema + migration: 3-shape detection (null→default record, flat
array→copy to all 3 built-in tabs, object→passthrough). Cap guard
iterates all record entries. Lock functions accept tabKey first param.

Components: RepoLockControls, EmptyLockedRepoRow gain tabKey prop.
Tab components use viewState.lockedRepos[tabKey()] ?? [] inline.
DashboardPage orphan cleanup includes lockedRepos keys.

Tests: 2446 passing (+13 net new). Per-tab isolation, cap enforcement,
migration shapes, cross-tab independence, reset cleanup.
Three independent reviewers flagged the shared array reference in the
flat-array migration path. Spread into independent arrays to prevent
latent mutation hazards if migrateLockedRepos is ever called without
subsequent Zod parsing.
Verifies that migrateLockedRepos returns independent array copies per
tab, not shared references. Addresses CR-005 from code review.
Cap guard now strips non-string array elements before Zod validates,
preventing corrupt localStorage entries from rejecting the entire
ViewState. Adds tests for custom tab pruning and partial object
migration passthrough.
- Extract REPO_STATE_TAB_IDS constant for resetViewState loops
- Export LOCKED_REPOS_CAP for test reference (no hardcoded 50)
- Fix misleading comment on migrateLockedRepos passthrough
- Reformat lock functions + moveTrackedItem to expanded style
- Replace algorithm-copy cap-guard test with integration tests
- Add custom tab lock mechanics tests for IssuesTab
- Add migrateLockedRepos mixed-type array filter test
@wgordon17 wgordon17 marked this pull request as ready for review April 23, 2026 18:32
@wgordon17 wgordon17 merged commit 3d73780 into gordon-code:main Apr 23, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Repo locks are shared globally instead of per-tab

1 participant